RTTI Delphi - Run Time Type Information - John COLIBRI. |
- résumé : présentation et utilisation de la RTTI avec Delphi : RTTI Delphi 1, RTTI Delphi 2010: tRttiContext, exploration d'objets, architecture tRttiType, tRttiMember et tValue, example typiques : sérialisation
d'objets .TXT, databinding, appels par Invoke, IOC et Dependency Injection. Diagrammes de classe UML
- mots clé : RTTI Delphi 2010, tRttiContext, tRttiType, tRttiMember,
tValue, sérialisation d'objets, databinding, Invoke, IOC, Dependency Injection, scripts, ORM, Make
- logiciel utilisé : Windows XP personnel, Delphi Xe2
- matériel utilisé : Pentium 2.800 Mhz, 512 Meg de mémoire, 250 Giga disque dur
- champ d'application : Delphi 1 ("old RTTI") et Delphi 2010, Delphi XE, Delphi XE2, Delphi XE3 sur Windows
- niveau : développeur Delphi
- plan :
1 - Definition RTTI RTTI (Run Time Type Information) permet d'analyser les types et des données à l'exécution. Par exemple nous pouvons récupérer la liste des
propriétés d'un tButton (le type), ou la valeur de Form1.Left (la valeur de la propriété d'un objet). Cette mécanique a été mise en place depuis Delphi 1, et permettait à l'époque
la gestions des .DFM. Lors du lancement d'une application, l'.EXE charge le .DFM (stocké sous forme de resource dans l'.EXE) et construit les formes, les composants et initialise leurs propriétés.
Il s'agit en fait de "méta données" sur les types et les objets. Les langages comme Java ou C# on généralisé ces possibilités, qui sont appelées "introspection". Pour les applications simples, nous n'avons pas besoin de RTTI. RTTI est utilisé:
- pas les constructeurs de frameworks, comme dUnit, Mock, ORM (Object Relational Mapping), protocoles de communications entre PC (Services Web, par exemple)
- par les applications importantes pour lesquelles nous pouvons factoriser notre code en implémentant tout ou partie des frameworks cités ci-dessus, en permettant de généraliser certains traitements qui étaient spécifiques à chaque classe.
2 - Exemple de base RTTI 2.1 - Rtti Delphi 1 Nous utilisons régulièrement RTTI sous Delphi 1 pour récupérer le nom des énumérés en utilisant la fonction GetEnumName.
Voici un exemple simple ou nous affichons Align pour chaque contrôle d'une tForm:
2.2 - RTTI Delphi 2010 2.2.1 - Utilisation de base
La technique de base pour analyser les types et les valeurs des objets avec la nouvelle version de RTTI est
Pour récupérer les informations de type d'un tButton, par exemple, nous pouvons utiliser
Procedure TForm1.tbutton_rtti_Click(Sender: TObject);
Var l_rtti_context: tRttiContext;
l_c_button_rtti_type: tRttiType;
l_c_rtti_field: tRttifield; Begin
l_c_button_rtti_type:= l_rtti_context.GetType(tButton);
For l_c_rtti_field In l_c_button_rtti_type.GetFields Do
display(l_c_rtti_field.ToString);
End; // tbutton_rtti_Click | dont voici le résultat: où l_c_rtti_field.ToString affiche "au mieux" les informations de chaque champ (le nom, le type, le décalage par rapport au début de l'objet)
2.3 - Explorateur d'Objets
Si nous partons d'un objet, nous pouvons utiliser son type, défini dans tObject.ClassType et à partir de là afficher ses champs, méthodes et propriétés.
De plus, comme nous avons des données en mémoire (un tObject a été créé), nous pouvons pour les champs et les propriétés récupérer leur valeur par des méthodes GetValue
L'utilisation classique est l'affichages des informations sur les composants d'une Forme: le noms des contrôles, leurs champs, méthodes et propriétés Par exemple
Notez que nous avons utilisé tRttiType.TypeKind, qui est un énuméré, pour
tester si la valeur est affichable simplement. En effet si la propriété est de type Record ou Classe, il faudrait récurser pour trouver les valeurs simple (possible mais trop lourd pour cet exemple simple).
Les valeurs de tTypeKind sont:
TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat,
tkString, tkSet, tkClass, tkMethod, tkWChar, tkLString, tkWString,
tkVariant, tkArray, tkRecord, tkInterface, tkInt64, tkDynArray, tkUString,
tkClassRef, tkPointer, tkProcedure); |
Comme de plus tRttiType a un champ BaseType qui désigne la Classe ancêtre, nous pouvons récurser pour trouver la liste des ancêtres:
Procedure fill_method_list(p_c_object: tObject; p_c_strings: tStrings);
Var l_rtti_context: tRttiContext;
l_c_rtti_type: tRttiType;
l_c_rtti_method: tRttiMethod; Begin
p_c_strings.Clear;
l_c_rtti_type:= l_rtti_context.GetType(p_c_object.ClassType);
For l_c_rtti_method In l_c_rtti_type.GetMethods Do
p_c_strings.Add(l_c_rtti_method.ToString);
End; // fill_method_list |
Voici l'affichage des champs de CheckBox1, par exemple:
Et pour terminer, comme les champs et les propriétés ont un GetValue, ils ont naturellement un SetValue :
- un click sur une propriété de l'objet place la valeur de la propriété dans un tEdit
Function f_property_string_value(p_c_object: tObject; p_property_display: String): String;
Var l_index: integer;
l_property_name: String;
l_rtti_context: tRttiContext;
l_c_rtti_type: tRttiType;
l_c_rtti_property: tRttiProperty; Begin
Result:= '?';
l_index:= 1; l_property_name:= '';
While (l_index<= Length(p_property_display)) And (p_property_display[l_index]<> ' ') Do
Begin
l_property_name:= l_property_name+ p_property_display[l_index];
inc(l_index); End;
l_c_rtti_type:= l_rtti_context.GetType(p_c_object.ClassType);
For l_c_rtti_property In l_c_rtti_type.GetProperties Do
Begin
If SameText(l_property_name, l_c_rtti_property.Name)
Then Begin
If l_c_rtti_property.PropertyType.TypeKind= tkInteger
Then Result:= l_c_rtti_property.GetValue(p_c_object).ToString
Break; End;
End; End; // f_property_string_value
Procedure TForm1.properties_listbox_Click(Sender: TObject);
Begin With properties_listbox_ Do
property_value_edit_.Text:=
f_property_string_value(f_c_listbox_object, Items[ItemIndex]);
End; // properties_listbox_Click | - et le clic d'un bouton modifie cette propriété
Procedure set_property_value(p_c_object: tObject;
p_property_display, p_string_value: String);
Var l_index: integer;
l_property_name: String;
l_rtti_context: tRttiContext;
l_c_rtti_type: tRttiType;
l_c_rtti_property: tRttiProperty; Begin
l_index:= 1; l_property_name:= '';
While (l_index<= Length(p_property_display)) And (p_property_display[l_index]<> ' ') Do
Begin
l_property_name:= l_property_name+ p_property_display[l_index];
inc(l_index); End;
l_c_rtti_type:= l_rtti_context.GetType(p_c_object.ClassType);
For l_c_rtti_property In l_c_rtti_type.GetProperties Do
Begin
If SameText(l_property_name, l_c_rtti_property.Name)
Then Begin
If l_c_rtti_property.PropertyType.TypeKind= tkInteger
Then l_c_rtti_property.SetValue(p_c_object, StrToInt(p_string_value));
Break; End;
End; End; // set_property_value
Procedure TForm1.set_property_value_Click(Sender: TObject);
Begin With properties_listbox_ Do
set_property_value(f_c_listbox_object, Items[ItemIndex], property_value_edit_.Text);
End; // set_property_value_Click | - dans notre exemple d'exécution, nous cliquons sur Left, qui a la valeur 8,
et lorsque nous entrons la valeur 50, CheckBox1 se déplace :
3 - Architecture RTTI 3.1 - Le Diagramme de Classe UML Mes premières impressions pour RTTI étaient que - pour Delphi 1 il s'agissait d'une organisation ad hoc un peu bricolée, et
que les développeurs n'ont pas arrêté d'étendre pour essayer d'extraire plus d'informations sur les types des classes
- pour Delphi 2010, une mécanique très complète, mais ayant trop de détails.
Utiliser RTTI sans partir d'une démo semblait assez osé.
Pour simplifier l'utilisation de RTTI, le mieux est de présenter directement les diagrammes de classe UML pour avoir une vue d'ensemble. Voici l'organisation de base:
Le premier objectif de RTTI est de modéliser les types Delphi, et essentiellement pour les Classes (pas les variables ou les procédures
globales). Voici une Classe simple: Type c_person=
Class m_age: Integer;
m_first_name: String;
m_salary: Double;
Constructor create_person(p_name: String;
p_age: integer; p_salary: Double);
Function f_display_person: String;
Property FirstName: String Read m_first_name Write m_first_name;
End; // c_person | Les informations sur cette Classe devront comprendre
- la Classe, c_person. Donc il faut un type TRttiInstanceType
- puis ses champs: m_age. Comme c'est un entier, il faut une classe pour représenter cet entier (valeur min, max etc). Donc tRttiOrdinalType
- pour m_name, nous pouvons souhaiter explorer le type de String (Ansi, WideString, UnicodeString etc)
- il y a un Constructor et une Function. Il faut donc des informations sur
le type de méthode (Constructor, Procedure, Function, Destructor). De plus est-ce une méthode de Classe, Virtual, pour une fonction, quel est le type de résultat. Et s'il y a des paramètres, il faut pouvoir les
analyser. Paramètre valeur, Var, Const, sans type ?
- et pour les Propertys, leur type, naturellement, mais aussi Read / Write, Default, index éventuel etc
En résumé, la hiérarchie RTTI doit donc modéliser toute la syntaxe des types Delphi.
3.2 - Technique d'extraction de type Comme indiqué plus haut, pour avoir des informations sur un type (essentiellement les Classes):
- nous définissons une variable de type tRttiContext
- cette variable nous permet de récupérer un tRttiType
- à partir de ce type, nous pouvons énumérer les champs, les méthodes et les propriétés
3.3 - RttiContext tRttiContext est donc indispensable pour explorer les types. Ce type Record est essentiellement chargé de gérer un pool d'objet pour optimiser la
construction et la consommation mémoire des objets tRtti_xxx. Comme l'expliquait Barry KELLY (le concepteur de la nouvelle RTTI) : - il est fréquent que les objets RTTI ne soient pas nécessaires après des
requêtes locales. Une gestion par Free serait fastidieuse
- l'utilisation d'Interfaces avec libération automatique manque aussi de flexibilité
- tRttiContext est la solution retenue permettant l'utilisation d'un Pool
Lorsque notre variable de type tRttiContext quitte la portée, les objets du cache qui avaient été créés en utilisant ce contexte sont libérés.
Comme c'est un Record, nous n'avons pas le besoin de le créer. Ni de le libérer Néanmoins, si nous souhaitons créer dans une Procedure des objets tRtti_xxx,
et les utiliser plus tard dans une autre Procedure, il faut que notre Record RTTI soit encore disponible. Dans ce cas, nous pouvons gérer explicitement le contexte ainsi que les objets qu'il a aidé à construire :
Var g_c_rtti_context: tRttiContext;
g_c_rtti_context:= tRttiContext.Create; // ooo
g_c_rtti_context.Free; |
Je dois avouer que des Records avec des Create et des Free me laissent
assez perplexe, de même d'ailleurs qu'un Record muni de Procedures et Funcitions. Je préférais les Records qui étaient des données sans code. Enfin ...
3.4 - tRttiMember
Une fois récupéré le tRttiType d'une Classe, nous analysons le contenu de cette Classe. Donc les Classes RTTI d'analyse des membres d'une Classe sont essentiels. Leur diagramme de Classe UML est le suivant
Quelques remarques:
3.5 - Valeurs des champs et propriétés - tValue Si nous utilisons RTTI pour explorer la valeur des données d'un objet, tRttiField et tRttiProperty possèdent donc des GetValue et SetValue.
Ces méthodes manipulent un type tValue. tValue a été créé pour gérer à la fois les valeurs et leur type. Voici un résumé sous forme de diagramme de classe UML:
tValue est un tuple de (valeur de donnée / information de type). tValue comporte un champ privé de type tValueData qui comporte effectivement un
pTypeInfo et une zone de donnée contenant la valeur.
3.5.1 - tValue.Make Make permet de construire un record tValue, et nous pouvons ensuite
transférer les données de ce tValue vers des données standard.
Class Procedure Make(ABuffer: Pointer; ATypeInfo: PTypeInfo; Out Result: TValue); Overload; Static;
Class Procedure Make(AValue: NativeInt; ATypeInfo: PTypeInfo; Out Result: TValue); Overload; Static;
|
Voici un exemple:
Procedure TForm1.make_integer_Click(Sender: TObject);
Var l_integer: Integer;
l_integer_tvalue: TValue;
l_integer_from_tvalue: Integer; Begin
l_integer:= 1234;
TValue.Make(@l_integer, TypeInfo(Integer), l_integer_tvalue);
display(l_integer_tvalue.ToString);
l_integer_from_tvalue:= l_integer_tvalue.AsInteger;
End; // make_integer_Click | Notez que - nous avons extrait l'entier par AsString.
- pour tValue, As n'extrait que les données du type annoncé. Nous n'aurions PAS pu stocker '123' et extraire AsInteger
Pour les données plus complexes, comme un Record, nous pouvons extraire les
données par ExtractRawData (une sorte de Move):
Type t_price= Record
m_amount: Double;
m_currency: String[9];
End;
Procedure TForm1.extract_data_Click(Sender: TObject);
Var l_price: t_price;
l_price_rawdata: t_price;
l_price_tvalue: TValue; Begin
l_price.m_amount:= 128.25;
l_price.m_currency:= 'yen';
TValue.Make(@l_price, TypeInfo(TRect), l_price_tvalue);
l_price_tvalue.ExtractRawData(@l_price_rawdata);
display(Format('%g %s', [l_price_rawdata.m_amount, l_price_rawdata.m_currency]));
End; // extract_data_Click | Nous pouvons aussi construire le Record par Make et lui affecter des données;
Type t_string_9= String[9];
Procedure TForm1.make_record_Click(Sender: TObject);
Var l_rtti_context: TRttiContext;
l_price_tvalue: TValue;
l_tprice_rtti_type: tRttiType;
l_currency: String[9]; l_price: t_price;
Begin // -- create the empty record tValue
TValue.Make(Nil, TypeInfo(TRect), l_price_tvalue);
// -- set the values of the tValue
l_tprice_rtti_type:= l_rtti_context.GetType(TypeInfo(t_price));
l_tprice_rtti_type.GetField('m_amount').SetValue(
l_price_tvalue.GetReferenceToRawData, 10); (* l_currency:= 'dollar';
l_rtti_context.GetType(TypeInfo(t_price)).GetField('m_currency').SetValue( l_price_tvalue.GetReferenceToRawData, t_string_9(l_currency)); *)
// -- transfer to some standard t_price
l_price_tvalue.ExtractRawData(@l_price);
display(Format('%g %s', [l_price.m_amount, l_price.m_currency]));
End; // make_record_Click | Notez que nous ne sommes pas parvenus à affecter une valeur au champ String (même en surtypant)
3.5.2 - tValue.From Nous pouvons récupérer un tValue en utilisant différentes fonctions From.
Class Function FromVariant(Const Value: Variant): TValue; Static;
Class Function From<T>(Const Value: T): TValue; Static;
Class Function FromOrdinal(ATypeInfo: PTypeInfo; AValue: Int64): TValue; Static;
Class Function FromArray(ArrayTypeInfo: PTypeInfo; Const Values: Array Of TValue): TValue; Static;
|
Voici une extraction simple:
Procedure TForm1.from_type_Click(Sender: TObject);
Var l_price: t_price;
l_price_tvalue: TValue;
l_price_rawdata: t_price; Begin
l_price.m_amount:= 128.25;
l_price.m_currency:= 'yen';
l_price_tvalue:= TValue.From<t_price>(l_price);
display(f_display_TF(l_price_tvalue.IsType<t_price>));
l_price_rawdata:= l_price_tvalue.AsType<t_price> ;
display(Format('%g %s', [l_price_rawdata.m_amount, l_price_rawdata.m_currency]));
End; // from_type_Click | et avec un variant:
Procedure TForm1.from_variant_Click(Sender: TObject);
Var l_variant: Variant;
l_tvalue: TValue; Begin
l_variant:= 'trial';
l_tvalue:= TValue.FromVariant(l_variant);
display(Format('%-15s %-15s %s',
[f_display_type_kind(l_tvalue.Kind), l_tvalue.ToString, l_tvalue.AsType< variant>]));
l_variant:= 4321;
l_tvalue:= TValue.FromVariant(l_variant);
display(Format('%-15s %-15s %d',
[f_display_type_kind(l_tvalue.Kind), l_tvalue.ToString, 3])); //l_tvalue.AsType< variant>]));
End; // from_variant_Click | ou un variant comme type générique
Procedure TForm1.from_T_Click(Sender: TObject);
Var l_variant: Variant;
l_tvalue: TValue; l_integer: Integer;
Begin l_variant:= 'trial';
l_tvalue:= TValue.From< variant> (l_variant);
display(Format('%-15s %s',
[f_display_type_kind(l_tvalue.Kind), l_tvalue.AsType< variant>]));
l_variant:= 4321;
l_tvalue:= TValue.From< variant> (l_variant);
(* display(Format('%s', [l_tvalue.AsType< variant>])); *)
l_integer:= l_tvalue.AsType< variant>;
display(Format('%-15s %d',
[f_display_type_kind(l_tvalue.Kind), l_integer]));
End; // store_variant_Click |
3.5.3 - Résumé sur tValue tValue
- n'est pas prévu pour supporter
- les opérateurs
- les méthodes
- n'est pas un remplacement du type Variant
- permet les conversions
- implicites chaque fois que c'est possible
- a des conversions génériques pour les autres types (As_xxx)
4 - Exemples Rtti Delphi 4.1 - Sérialisation d'Objets
L'exemple le plus classique d'utilisation de RTTI est la sérialisation d'objets. Nous souhaitons transformer un objet mémoire en un série d'octets et pouvoir reconstruire l'objet à partir de ces octets. La sérialisation peut se
faire vers un fichier .TXT, un fichier .XML, un tStream mémoire (pour être envoyé sur le réseau) etc.
Voici un exemple simple qui sérialise les Propertys d'un tObject:
Type c_serializer=
Class
Function f_serialize(p_c_object: tObject): String;
End; // c_serializer
Const // -- to know those, look at tValue.ToString
k_not_serializable= [tkUnknown, tkSet,
tkClass, tkMethod,
tkVariant, tkArray, tkRecord, tkInterface, tkDynArray,
tkClassRef, tkPointer, tkProcedure];
k_toquote_kind= [tkChar, tkString, tkWChar, tkLString, tkWString,
tkUString];
Function c_serializer.f_serialize(p_c_object: tObject): String;
Function c_serializer.f_serialize_property(p_tvalue: tValue): String;
Begin If p_tvalue.IsEmpty
Then Result:= 'Nil'
Else Result:= p_tvalue.ToString;
If p_tvalue.Kind In k_toquote_kind Then
Result:= QuotedStr(Result);
End; // f_serialize_property
Var l_c_result_list: tStringList;
l_c_object_type: tRttiType;
l_rtti_context: tRttiContext;
l_c_rtti_property: tRttiProperty;
l_tvalue: tValue; Begin // f_serialize
If p_c_object= Nil
Then Exit('');
l_c_result_list:= tStringList.Create;
l_c_object_type:= l_rtti_context.GetType(p_c_object.ClassType);
For l_c_rtti_property In l_c_object_type.GetProperties Do
If l_c_rtti_property.IsReadable And l_c_rtti_property.IsWritable
And Not (l_c_rtti_property.PropertyType.Handle.Kind In k_not_serializable)
Then Begin
l_tvalue:= l_c_rtti_property.GetValue(p_c_object);
l_c_result_list.Values[l_c_rtti_property.Name]:= f_serialize_property(l_tvalue);
End;
Result:= l_c_result_list.Text;
l_c_result_list.Free; End; // f_serialize |
et la classe symétrique qui désérialise: Type c_deserializer=
Class
Function f_deserialize_property(p_value: String; p_typeinfo: pTypeInfo): tValue;
Procedure deserialize(p_string: String; p_c_object: tObject);
End; // c_deserializer
Procedure c_deserializer.deserialize(p_string: String;
p_c_object: tObject);
Function f_deserialize_property(p_value: String;
p_typeinfo: pTypeInfo): tValue; Begin
Case p_typeinfo.Kind Of
tkInteger: Result:= StrToInt(p_value);
tkInt64: ;
tkEnumeration: Result:= tValue.FromOrdinal(p_typeinfo, System.TypInfo.GetEnumValue(p_typeinfo, p_value));
tkFloat: Result:= StrToFloat(p_value);
tkChar, tkWChar, tkLString, tkWString, tkUString,
tkString: Result:= p_value;
End; End; // f_deserialize_property
Var l_c_input_list: tStringList;
l_rtti_context: tRttiContext;
l_c_object_rtti_type: tRttiType;
l_list_index: integer;
l_c_rtti_property: tRttiProperty;
l_property_name, l_property_value: String;
l_tvalue: tValue; l_string: String;
Begin // deserialize
l_c_input_list:= tStringList.Create;
l_c_input_list.Text:= p_string;
l_c_object_rtti_type:= l_rtti_context.GetType(p_c_object.ClassType);
For l_list_index:= 0 To l_c_input_list.Count- 1 Do
Begin // -- "Caption='Ok'"
// -- form the property name, get the tRttiProperty
l_property_name:= l_c_input_list.Names[l_list_index];
l_c_rtti_property:= l_c_object_rtti_type.GetProperty(l_property_name);
// -- from the value, build a tValue
If (Self= Nil) Or (l_c_rtti_property.PropertyType.Handle.Kind In k_not_serializable)
Then Continue;
// -- from the string value, get the tValue
l_property_value:= l_c_input_list.ValueFromIndex[l_list_index];
l_tvalue:= f_deserialize_property(l_property_value, l_c_rtti_property.PropertyType.Handle);
// -- set the object property value
If Not l_tvalue.IsEmpty
Then Begin
If l_c_rtti_property.PropertyType.Handle.Kind In k_toquote_kind
Then Begin
l_string:= l_tvalue.AsString;
l_c_rtti_property.SetValue(p_c_object, AnsiDequotedStr(l_string, ''''))
End
Else l_c_rtti_property.SetValue(p_c_object, l_tvalue)
End
Else ; // display('value empty');
End; // for l_list_index l_c_input_list.Free;
End; // deserialize |
Nous pouvons alors sérialiser un objet c_person
Procedure TForm1.create_and_serialze_person_Click(Sender: TObject);
Var l_c_person: c_person;
l_c_serializer: c_serializer; Begin
l_c_person:= c_person.create_person('matt', 45, 5461.33);
l_c_serializer:= c_serializer.Create;
g_serialized_person:= l_c_serializer.f_serialize(l_c_person);
l_c_serializer.free; l_c_person.Free;
End; // create_and_serialze_person_Click
Procedure TForm1.deserialize_person_Click(Sender: TObject);
Var l_c_person: c_person;
l_c_deserializer: c_deserializer; Begin
l_c_person:= c_person.create_person('smith', 33, 4567.00);
l_c_person:= c_person.create_person('', 0, 0);
l_c_deserializer:= c_deserializer.Create;
l_c_deserializer.deserialize(g_serialized_person, l_c_person);
display(l_c_person.f_display_person);
l_c_deserializer.free; l_c_person.Free;
End; // deserialize_person_Click | et voici un exemple de sérialisation
ou un tButton, en changeant la valeurs de propriétés avant la désérialisation:
Var g_c_button_text: tStringList;
Procedure TForm1.serialize_button_Click(Sender: TObject);
Var l_c_serializer: c_serializer; Begin
l_c_serializer:= c_serializer.Create;
g_c_button_text:= tStringList.Create;
g_c_button_text.Text:= l_c_serializer.f_serialize(Sender);
display(g_c_button_text.Text);
End; // serialize_button_Click
Procedure TForm1.deserialize_button_Click(Sender: TObject);
Var l_c_deserializer: c_deserializer; Begin
With g_c_button_text Do
Values['Left']:= '50';
l_c_deserializer:= c_deserializer.Create;
l_c_deserializer.deserialize(g_c_button_text.Text, serialize_button_);
End; // deserialize_button_Click |
4.2 - DataBinding
La "liaison des données" consiste à affecter à la propriété d'un tObject la même valeur d'un propriété d'un autre tObject. C'est ce que font les composants dbEdit, dbGrid etc, en propageant les
valeurs des tFields vers des propriétés d'affichage des contrôles. Nous pouvons naturellement simplement affecter la valeur d'une propriété à un tEdit:
first_name_edit.Text:= g_c_person.FirstName
| mais le but d'une mécanique de databinding est d'automatiser ces affectations.
De nombreuses solutions sont possibles - nous pouvons doter les objets d'une mécanique de liaison, mais cela oblige
nos objets à descendre d'objets liables
- plus général, nous pouvons créer des Interfaces, et utiliser des objets qui implémentent ces Interfaces
- si nous ne souhaitons imposer aucune contrainte à nos objets, nous pouvons
créer une liste des liaisons qui soit indépendante des objets
- nous pouvons aussi utiliser une solution dissymétrique: un objet possède la liste des objets à mettre à jour. C'est cette solution que nous avons adoptée ici
Voici nos Classes:
Type c_origin_object= Class; // forward
c_binding_info= Class
m_c_origin_object: c_origin_object;
m_origin_property_name: String;
m_c_target_object: tObject;
m_target_property_name: String;
Constructor Create(
p_c_origin_object: c_origin_object;
p_origin_property_name: String;
p_c_target_object: tObject;
p_target_property_name: String);
Function f_bind_info: String;
End; // c_binding_info c_origin_object=
Class(tObject)
m_c_binding_info_list: TList<c_binding_info> ;
Constructor Create;
Procedure add_target_object(
p_c_origin_object: c_origin_object; p_origin_property_name: String;
p_c_target_object: tObject; AControlProperty: String);
Procedure update_target_values;
Destructor Destroy; Override;
End; // c_origin_object | Notez que
- comme l'objet origine contient la liste des objets destination, c_binding_info n'a pas réellement besoin de conserver une référence vers l'objet origine
Voici l'implémentation
// -- c_binding_info
Constructor c_binding_info.Create(
p_c_origin_object: c_origin_object; p_origin_property_name: String;
p_c_target_object: tObject; p_target_property_name: String);
Begin Inherited Create;
m_c_target_object:= p_c_target_object;
m_target_property_name:= p_target_property_name;
m_c_origin_object:= p_c_origin_object;
m_origin_property_name:= p_origin_property_name; End; // Create
Function c_binding_info.f_bind_info: String; Begin
Result:=
m_c_origin_object.ClassName+ '.'+ m_origin_property_name
+ '->' // + Control.Name+ '.'+ ControlPropertyName;
End; // f_bind_info // -- c_origin_object
Constructor c_origin_object.Create; Begin
Inherited;
m_c_binding_info_list:= TList<c_binding_info>.Create;
End; // Create
Procedure c_origin_object.add_target_object(
p_c_origin_object: c_origin_object; p_origin_property_name: String;
p_c_target_object: tObject; AControlProperty: String);
Begin m_c_binding_info_list.Add(
c_binding_info.Create(
Self, p_origin_property_name,
p_c_target_object, AControlProperty));
End; // add_target_object
Procedure c_origin_object.update_target_values;
// -- c_person.m_name -> tNameEdit.Text
Var l_rtti_context: tRttiContext;
l_c_origin_object_rtti_type: tRttiType;
l_c_origin_object_rtti_property: tRttiProperty;
l_c_binding_info: c_binding_info;
l_c_target_object: tObject;
l_c_target_object_rtti_type: tRttiType;
l_c_target_object_rtti_property: tRttiProperty;
l_origin_tvalue: tValue; Begin
l_rtti_context:= tRttiContext.Create;
// -- scan the origin for the origin property names
l_c_origin_object_rtti_type:= l_rtti_context.GetType(Self.ClassType);
If l_c_origin_object_rtti_type= Nil
Then Begin
display('Nil'); Exit;
End;
For l_c_origin_object_rtti_property In l_c_origin_object_rtti_type.GetProperties Do
Begin display(l_c_origin_object_rtti_property.ToString);
For l_c_binding_info In m_c_binding_info_list Do
If SameText(l_c_binding_info.m_origin_property_name, l_c_origin_object_rtti_property.Name)
Then Begin
display(' same '+ l_c_binding_info.m_origin_property_name);
l_c_target_object:= l_c_binding_info.m_c_target_object;
l_c_target_object_rtti_type:= l_rtti_context.GetType(l_c_target_object.ClassType);
l_origin_tvalue:= l_c_origin_object_rtti_property.GetValue(Self);
For l_c_target_object_rtti_property In l_c_target_object_rtti_type.GetProperties Do
If SameText(l_c_binding_info.m_target_property_name, l_c_target_object_rtti_property.Name)
Then Begin
display(' same '+ l_c_binding_info.m_target_property_name);
l_c_target_object_rtti_property.SetValue(l_c_target_object, l_origin_tvalue);
End;
End;
End; // for l_c_origin_object_rtti_property
l_rtti_context.Free; End; // update_target_values
Destructor c_origin_object.Destroy; Begin
m_c_binding_info_list.Free; Inherited;
End; // Destroy |
A titre d'exemple une c_person qui peut être lié à d'autres objets:
Type c_person= // one "person"
Class(c_origin_object)
m_name: String;
m_age: integer;
Constructor create_person(p_name: String;
p_age: integer);
Procedure set_name(p_name: String);
Procedure set_age(p_age: Integer);
Property Name: String Read m_name Write m_name;
Property Age: Integer Read m_age Write set_age;
End; // c_person
Constructor c_person.create_person(p_name: String; p_age: integer);
Begin Inherited create;
m_name:= p_name; m_age:= p_age;
End; // create_person
Procedure c_person.set_name(p_name: String);
Begin m_name:= p_name; update_target_values
End; // set_name
Procedure c_person.set_age(p_age: Integer);
Begin m_age:= p_age; update_target_values
End; // set_age |
et voici un exemple avec liaison à un tEdit et un tTrackBar:
Var g_c_person: c_person= Nil;
Procedure TForm1.create_person_Click(Sender: TObject);
Begin
g_c_person:= c_person.create_person('joe', 41);
End; // create_person_Click
Procedure TForm1.bind_controls_Click(Sender: TObject);
Begin With g_c_person Do
Begin
add_target_object(g_c_person, 'Name', name_edit_, 'Text');
add_target_object(g_c_person, 'Age', age_trackbar_, 'Position');
update_target_values; End;
End; // bind_controls_Click
Procedure TForm1.set_random_age_Click(Sender: TObject);
Begin g_c_person.Age:= 20+ Random(75);
End; // set_random_age_Click | et un exemple d'exécution
Notez que - pour trouver la propriété origine, l'objet destination et la propriété destination, nous effectuons chaque fois des boucles For qui sont très peu efficaces
- si nous souhaitions mémoriser les liens, il faudrait que le tRttiContext soit sauvegardé, soit dans l'objet origine, soit dans un objet global
- nous n'avons sérialisé que les types simples (Integer, String etc). Si
nous souhaitons sérialiser les Records ou les Classes, il faut récurser, et le problème devient plus volumineux
- de nombreuses librairies incluent déjà la sérialisation, en mode .TXT ou en mode .XML
4.3 - RTTI Invoke A partir d'un tRttiMethod, nous pouvons appeler Invoke, avec les paramètres éventuels.
Function Invoke(Instance: TObject; Const Args: Array Of TValue): TValue; Overload;
Function Invoke(Instance: TClass; Const Args: Array Of TValue): TValue; Overload;
Function Invoke(Instance: TValue; Const Args: Array Of TValue): TValue; Overload;
| et la possibilité d'utiliser comme premier paramètre une référence de Classe correspond aux méthodes de Classe (comme le Constructor, entre autres).
4.3.1 - Appel de méthodes d'un objet Voici un exemple où nous appelons quelques objets de notre Classe c_person:
Type c_person= Class
m_name: String;
m_age: integer;
m_salary: Double;
Constructor create_person(p_name: String;
p_age: integer; p_salary: Double);
Function ToString: String; Override;
Destructor Destroy; Override;
End; // c_person
Constructor c_person.create_person(p_name: String;
p_age: integer; p_salary: Double); Begin
Inherited create; m_name:= p_name;
m_age:= p_age; m_salary:= p_salary;
End; // create_person
Procedure c_person.increment_age(p_delta: Integer);
Begin Inc(m_age, p_delta);
End; // increment_age
Function c_person.ToString: String; Begin
Result:= Format('%-10s %2d %7.2f', [m_name, m_age, m_salary]);
End; // ToString
Function c_person.f_c_self: c_person; Begin
Result:= Self; End; // f_c_self
Procedure TForm1.invoke_methods_Click(Sender: TObject);
Var l_rtti_context: TRttiContext;
l_c_person: c_person;
l_display_tvalue: tValue;
l_c_person_rtti_type: tRttiType; Begin
l_rtti_context:= TRttiContext.Create;
l_c_person_rtti_type:= l_rtti_context.GetType(c_person);
display(f_display_type_kind(l_c_person_rtti_type.TypeKind));
l_c_person := c_person.Create;
l_display_tvalue:= l_c_person_rtti_type.GetMethod('f_display_person').Invoke(l_c_person, []);
display(l_display_tvalue.AsString);
display(l_c_person_rtti_type.GetMethod('f_display_person').Invoke(l_c_person, []).AsString);
l_c_person_rtti_type.GetMethod('increment_age').Invoke(l_c_person, [3]);
End; // invoke_methods_Click | Dans cet exemple, nous créons un objet c_person normal, et appelons quelques
procédures en utilisant RTTI. Si déjà nous avons accès à c_person, il semble très alambiqué compliquer de passer par RTTI pour appeler une méthode §
4.3.2 - Utilisation de Invoke pour un Constructor
Néanmoins, nous pouvons utiliser RTTI pour invoquer le Constructor. Dans ce cas nous pouvons créer un tObject du bon type, puis appeler les méthodes de cet objet. Voici le code;
Procedure TForm1.invoke_constructor_Click(Sender: TObject);
Var l_rtti_context: TRttiContext;
l_c_rtti_type: tRttiType;
l_c_person_rtti_instance_type: TRttiInstanceType;
l_c_class_ref: tClass; l_c_person: tObject;
Begin l_rtti_context:= TRttiContext.Create;
l_c_rtti_type:= l_rtti_context.FindType('u_c_person_6.c_person');
display(f_display_type_kind(l_c_rtti_type.TypeKind));
l_c_person_rtti_instance_type:= l_c_rtti_type As TRttiInstanceType;
l_c_class_ref:= l_c_person_rtti_instance_type.MetaclassType;
l_c_person:= l_c_person_rtti_instance_type.GetMethod('create_person').Invoke(
l_c_class_ref, ['joe', 15, 500]).AsType<c_person>;
display(l_c_person.ToString);
End; // invoke_constructor_Click | et son exécution :
Notez que - l'identificateur c_person n'est mentionné nulle part, et pourtant nous avons pu appeler les méthodes sur un tObject
- l'appel d'un Constructor par Invoque a les mêmes fonctionnalités qu'un appel de Constructor normal (appel de NewInstance, AfterConstruction etc)
Ce sont ces possibilités qui nous ouvrent la voie vers
- la construction de scripts qui utilisent des Classes que nous avons construites
- plus généralement toutes les transformations "nom de classe string -> objet", pour lesquelles nous utilisions en général la technique des
références de Classe et les Constructors Virtuals
Comme les Constructor Virtual sont une spécificité de Delphi, les autres langages utilisent en effet RTTI pour réaliser le même type de traitement
- à part les scripts, ces conversions "nom de classe string -> objet" peuvent alors être utilisés pour
- les plugins
- les frameworks dUnit ou Mock
- Invoke est aussi est nécessaire pour supporter les attributs personnalisés
4.3.3 - IOC - Inversion of Control Voici, à titre d'exemple un exemple d'inversion de contrôle en utilisant une
injection par le Constructor.
Supposons qu'une Classe c_consumer utilise d'une classe auxiliaire, mais que suivant le traitement elle fasse appel à différents descendants de cette classe
auxiliaire (une classe utilise un client TCP/IP, mais suivant le cas ce sera un client Mail ou un client HTTP). La technique de base est de - créer une Classe ancêtre, et que c_consumer n'utilise que cette Classe
ancêtre. Au moment de l'exécution nous fournirons le client qui nous intéresse
- créer une Classe abstraite, avec les mêmes constructions
- ou finalement utiliser un Interface, et fournir, pour l'exécution, l'objet
qui implémente l'Interface
Il est peu recommandé que c_consumer créé lui-même le bon client. Il vaut mieux que pour c_consumer nous utilisions "programming to the Interface", ce
qui laisse à l'utilisateur de notre c_consumer le choix du client qu'il veut utiliser. Le contrôle a été inversé: ce n'est pas c_consumer qui crée le client concret, c'est le programme appelant qui lui injecte le client à utiliser.
Voici - notre Interface, la classe utilisatrice, c_consumer, et une Classe qui se charge de construire la bonne classe utilitaire:
Type i_dependency= Interface
['{618030A2-DB17-4532-81D0-D5AA6F73DC66}']
Procedure compute;
End; // i_dependency c_consumer=
Class Private
m_i_dependency: i_dependency;
Public
Constructor create_consumer(p_i_dependency: i_dependency);
Procedure do_compute;
End; // c_consumer c_dependency_injector=
Class Public
Class Function f_i_dependency(p_qualified_dependency_class_name: string):
i_dependency;
End; // c_dependency_injector // -- c_consumer
Constructor c_consumer.create_consumer(p_i_dependency: i_dependency);
Begin m_i_dependency:= p_i_dependency;
End; // create_consumer Procedure c_consumer.do_compute;
Begin If m_i_dependency<> Nil
Then m_i_dependency.compute;
End; // do_compute // -- c_dependency_injector
Class Function c_dependency_injector.f_i_dependency(p_qualified_dependency_class_name: string):
i_dependency; Var l_rtti_context: TRttiContext;
l_c_rtti_instance_type: TRttiInstanceType;
l_c_class_ref: tClass;
l_i_dependency: i_dependency; Begin
l_c_rtti_instance_type:= l_rtti_context.FindType(p_qualified_dependency_class_name)
As TRttiInstanceType;
l_c_class_ref:= l_c_rtti_instance_type.MetaclassType;
Result:= l_c_rtti_instance_type.GetMethod('create')
.Invoke(l_c_class_ref, []).AsInterface As i_dependency
End; // f_i_dependency | - puis un exemple de classe utilitaire:
Type c_auxiliary_one=
Class(TInterfacedObject, i_dependency)
Public
Procedure compute;
End; // c_auxiliary_one
Procedure c_auxiliary_one.compute; Begin
display('Instance of type c_auxiliary_one'); End; // compute
| - et le programme final:
Procedure TForm1.constructor_injection_Click(Sender: TObject);
Var l_i_dependency: i_dependency;
l_c_consumer: c_consumer; Begin
l_i_dependency:= c_dependency_injector
.f_i_dependency('u_c_auxiliary.c_auxiliary_one');
l_c_consumer:= c_consumer.create_consumer(l_i_dependency);
l_c_consumer.do_compute; l_c_consumer.Free;
End; // constructor_injection_Click | - et un exemple d'exécution:
5 - Remarques RTTI Delphi 2010 5.1 - RTTI Delphi 1
Delphi 1 utilisait déjà RTTI, essentiellement pour linéariser dans le .DFM les Propertys publiques. Puis d'autres fonctionnalités ont été ajoutées : - TYPINFO.PAS (l'unité initiale)
- n'utilise pas une mécanique objet
- utilise des types bas niveau, des Record variants, des Pointer
- est incomplet: pas les méta classes (Class Of) ni les Records
- INTFINFO.PAS
- est surtout optimisé pour Soap
- OBJAUTO.PAS
- apporte des information de type sur les méthodes
- n'est pas objet
- l'invocation se fait par des Variants
- optimisé pour supporter le scripting COM
Le diagramme de Classe suivant montre bien cette mécanique non objet à base de Record variants (union libre) et de pointeurs en tous genres
Mentionnons néanmoins que nous pouvions déjà effectuer certains traitements comme la linéarisation des objets ou certaines invocations.
5.2 - RTTI Delphi 2010 L'avantage de la nouvelle mécanique est
- une utilisation d'objets
- l'inclusion de tous les champs
- l'analyse des zones de visibilité autres que Public / Published
- la sûreté des types ("Type Safe")
- de permettre la métaprogrammation en utilisant les attributs (pas présenté ici)
5.3 - Volume de l'.EXE Les informations de RTTI sont stockées dans l'.EXE, qui a donc une taille plus importante.
Pour évaluer cette augmentation, nous avons utilisé une simple Classe:
Type c_person= Class
m_first_name: String;
m_current_age: integer;
m_salary_min: Double;
m_person_enumeration: t_person_enumeration;
Constructor create_person(p_name: String;
p_age: integer; p_salary: Double);
Function f_display_person: String;
Property FirstName: String Read m_first_name Write m_first_name;
Property CurrentAge: Integer Read m_current_age Write m_current_age;
Property SalaryMin: Double Read m_salary_min Write m_salary_min;
End; // c_person | La taille de l'.EXE est de 1.500 K environ.
Un programme qui affiche les séquences ASCII de ce fichier a détecté:
0015.1E4F PTypeInfo
0015.1EDE TDelegatedComparer
0015.1F00 PTypeInfo
0015.1FD8 MTDelegatedComparer
0015.201B PTypeInfo
0015.20A9 MTDelegatedComparer
0015.20EC PTypeInfo
0015.2474 m_first_name
0015.248C m_current_age
0015.24A5 m_salary_min
0015.24E1 create_person
0015.253F f_display_person
0015.25A2 c_person
0015.25B5 u_c_person
0015.25F7 FirstName
0015.261B CurrentAge
0015.2640 SalaryMin
0015.3063 Splitter1
0015.3100 Splitter1
0015.31AA exit_Click
0015.31BB clear_Click
0015.31CD FormCreate
0015.32AD exit_Click
0015.32E7 clear_Click
0015.3322 FormCreate
0015.34EF u_24_dump_rtti
0015.9195 u_c_person
0015.91A0 u_display_simple
0015.91C4 HelpIntfs
0015.91D5 RTLConsts
| et nous pouvons même effectuer un dump hexa de la zone RTTI de c_person en mémoire :
Quelques remarques:
- l'ensemble des types RTTI fait environ 19.000 lignes, soit environ 240K (sans les adresses hexa)
- nous avons remarqué plusieurs pTypeInfo. En fait dans notre simple .EXE
(une seule Classe tPerson), il y en a 91. Cette redondance a vraissemblablement été créée pour optimiser la vitesse plus que la place
- lors de nos essais, nous avons essayé de voir si des informations RTTI
étaient générées pour tous les Type (un Set Of, un Array etc.) En fait nous avons au début eu quelques difficultés car des informations sur les Classes n'apparaissaient pas toujours. En fait c'est le "Smart Linker" qui
supprimait toutes les informations RTTI sur les Classes (ou autres types) qui étaient uniquement définis mais jamais utilisés (sous forme de variable, paramètre etc)
Donc, si vous faites ce genre d'essai, pensez a utiliesr les types quelque
part.
5.4 - Limitation du volume de RTTI dans l'.EXE Pour limiter le volume des informations RTTI, nous pouvons placer au début du .DPR, avant toute clause Uses, les directives suivantes:
{$WEAKLINKRTTI ON} {$RTTI EXPLICIT METHODS([]) PROPERTIES([]) FIELDS([])} |
Ces directives - garantissent qu'aucune information RTTI ne sera générée pour nos propres unités ou les composants tiers compilés avec notre projet, sauf si nous
avons explicitement exigé que ces informations soient générées
- ne permettent pas de supprimer le RTTI de la RTL ou de la VCL
Compte tenu du volume RTTI constaté sur notre exemple précédent, cela peut
représenter 20 % de la taille de l'.EXE (1.356 / 1.575)
5.5 - Ce que RTTI ne permet pas Actuellement RTTI ne permet pas - l'analyze (invocation) de procédures globales
- l'analyze de tous les types globaux (il permet le traitement des énumérés, des Classes, des Records et des champs / méthodes / propriétés contenus dans les Classes / Records de n'importe quel type)
Il s'agit donc d'une mécanique fondamentalement associées aux objets.
5.6 - Autres Exemples d'utilisation Parmi les autres exemples mentionons - la possibilité d'étendre des types (ajouter des champs)
- les mappings de tous genres, permettant par exemple
- de créer des tDataSets à partir de Classes, ce qui est le départ vers les ORM
- l'association automatique de champs d'un objet à des contrôles visuels
(c_person.Age est automatiquement lié à age_edit_)
5.7 - Critiques sur RTTI Delphi 2010 Quelques critiques ont été adressées à la nouvelle version de RTTI
- la complexité de son architecture.
- le faible gain par rapport à ce qui était déjà possible avec le RTTI de Delphi 1
6 - Télécharger le code source Delphi
Vous pouvez télécharger:
Comme d'habitude: - nous vous remercions de nous signaler toute erreur, inexactitude ou
problème de téléchargement en envoyant un e-mail à jcolibri@jcolibri.com. Les corrections qui en résulteront pourront aider les prochains lecteurs
- tous vos commentaires, remarques, questions, critiques, suggestion d'article, ou mentions d'autres sources sur le même sujet seront de même les bienvenus à jcolibri@jcolibri.com.
- plus simplement, vous pouvez taper (anonymement ou en fournissant votre e-mail pour une réponse) vos commentaires ci-dessus et nous les envoyer en cliquant "envoyer" :
- et si vous avez apprécié cet article, faites connaître notre site, ajoutez un lien dans vos listes de liens ou citez-nous dans vos
blogs ou réponses sur les messageries. C'est très simple: plus nous aurons de visiteurs et de références Google, plus nous écrirons d'articles.
7 - Références Quelques références:
Le grand champion de RTTI Delphi 2010 est sans conteste Robert LOVE avec plus de 13 articles, parmi lesquels
Et sur ce site les articles suivants pour quelques techniques de base: - les Constructeurs Virtuels et les
Références de classes (VIRTUAL CONSTRUCTORS, CLASS OF) permettent la séparation entre un projet et son fichier .EXE et des modules compilés séparément et liés à l'exécution. Par conséquent le point de départ pour les
Framework Applicatifs et les Plugins
John COLIBRI - 12 mar 2007 - MyUtf : Environnement de Test Unitaire :
cet environnement de test possède une structure très simple (deux listes), tout en offrant toutes les fonctionnalités des outils de test unitaire traditionnels. Voici un bon point de départ pour débuter dans ce domaine,
comprendre la structure de tels outils ou comme point de départ pour améliorer ou transformer cet environnement en n'importe quel outil de traitement non invasif de parties de projets ou de librairies.
Felix COLIBRI - 26 jan 2009
- Les Génériques Delphi: exemple avec une tList<T>, création d'une pile, règles de compatibilité de type, génération du code,
types pouvant être génériques, contraintes Interface, Class, héritage, Constructor. Exemple Observateur et Calculateur. Interfacees et conteneurs génériques de la Vcl
John COLIBRI - 12 feb 2013
8 - L'auteur John COLIBRI est passionné par le développement Delphi et les applications de Bases de Données. Il a écrit de nombreux livres et articles, et partage son temps entre le développement de projets (nouveaux projets, maintenance, audit, migration BDE, migration Xe_n, refactoring) pour ses clients, le
conseil (composants, architecture, test) et la
formation. Son site contient des articles
avec code source, ainsi que le programme et le calendrier des stages de formation Delphi, base de données, programmation objet, Services Web, Tcp/Ip et
UML qu'il anime personellement tous les mois, à Paris, en province ou sur site client. |